Python 3.12 salió en octubre de 2023 y, a diferencia de versiones recientes más ruidosas, trajo una lista de mejoras sobrias pero sólidas: sintaxis nueva para genéricos, trazas de error que por fin sirven para depurar, subinterpretes con GIL propio en fase experimental y un rendimiento general en torno al 5% superior a 3.11. Un año después, con 3.13 ya publicado y su build sin GIL abriendo conversaciones interesantes, merece la pena detenerse en qué aportó 3.12 realmente al día a día y qué merece pereza o prisa a la hora de migrar.
Lo que cambia sin drama
La estrella de la release es la nueva sintaxis de parámetros de tipo, formalizada en PEP 695. Antes había que importar TypeVar desde typing, declarar la variable en módulo y pasarla a Generic[T] para cada clase genérica. Era ceremonioso y colocaba metadata de tipo lejos del sitio donde tenía sentido leerla. Con 3.12, los corchetes van directamente tras el nombre de la clase o función, en línea, y el intérprete se encarga de crear la variable con el scope correcto. El cambio parece estético, pero reduce fricción real cuando escribes librerías que abusan de genéricos, y además permite declarar alias de tipo con la nueva palabra clave type, que son evaluados de forma perezosa y soportan referencias hacia adelante sin necesidad de strings.
El segundo cambio que notas a las dos horas de usar 3.12 son los mensajes de error. Cuando llamas a un método mal escrito, el traceback ya no se limita a decir que el atributo no existe: sugiere el nombre más parecido, remarca el fragmento exacto con caret alignment y, en errores de importación, te indica si falta un paquete o si se trata de un ciclo. Parece cosmético, pero en una base de código grande ahorra minutos por sesión de depuración, especialmente cuando quien lee el error no es quien escribió el código.
Las f-strings recibieron otra mejora silenciosa gracias a PEP 701: ahora son expresiones Python completas. Se pueden anidar comillas del mismo tipo que las exteriores, dividir en varias líneas, incluir comentarios dentro de la interpolación y usar barras invertidas sin tener que recurrir al truco de asignar a una variable antes. El parser las trata como código normal, no como un mini-lenguaje aparte, lo cual además mejora los mensajes de sintaxis cuando te equivocas dentro de una f-string.
Rendimiento: expectativas realistas
El proyecto Faster CPython sigue dando sus frutos, pero conviene calibrar. Los 5% de mejora agregada frente a 3.11 que anuncia la documentación son reales en benchmarks como pyperformance: DocUtils sube cerca de un 10%, Django un 5% largo, aplicaciones asyncio algo menos. Las comprehensions simples pueden llegar a duplicar su velocidad porque se reescribieron para inlining interno. Sin embargo, código dominado por llamadas a extensiones en C (numpy, pandas en hot loops, ORMs que pasan tiempo en psycopg) apenas verá diferencia, porque el cuello de botella vive fuera del intérprete.
La lectura pragmática es que 3.12 no es una razón suficiente por sí misma para migrar si tu carga está en I/O o en C, pero tampoco requiere justificación si tu proyecto es mayoritariamente Python puro: el upgrade es gratis en ese escenario. En mi experiencia, migrar un proyecto Django de tamaño medio desde 3.11 a 3.12 llevó unas dos tardes, la mayoría dedicadas a rebuildar algunas extensiones C que no tenían wheels para 3.12 el primer mes tras el lanzamiento.
Subinterpretes y el futuro post-GIL
PEP 684 introdujo subinterpretes con GIL independiente por cada uno, y 3.12 los expone de forma experimental a través del módulo interpreters. La promesa es interesante: paralelismo real sin salir del proceso, con coste de arranque mucho menor que multiprocessing y sin la sobrecarga de serializar argumentos a través de pipes. La realidad en 2024 es que la API está en beta, que buena parte del ecosistema de extensiones C todavía no soporta múltiples interpretes en el mismo proceso y que los patrones de comunicación por canales están sin estabilizar.
Es más útil verlos como una piedra en el camino que como una herramienta de producción: preparan el terreno para la build free-threaded que 3.13 introduce como opción de compilación, donde el GIL directamente desaparece. Esa es la promesa mayor, pero también la que más impacto puede tener en bibliotecas que llevan veinte años asumiendo su existencia. El camino prudente es quedarse en 3.12 para trabajo real y jugar con 3.13t en ramas de experimento.
from typing import TypeVar, Generic
# Estilo pre-3.12: ceremonioso, TypeVar en módulo
T_old = TypeVar("T_old")
class Cache(Generic[T_old]):
def get(self, key: str) -> T_old: ...
# 3.12 con PEP 695: el genérico vive junto a la clase
class Cache[T]:
def get(self, key: str) -> T: ...
# Alias de tipo con evaluación perezosa
type Vector = list[float]
type JSON = dict[str, "JSON"] | list["JSON"] | str | int | float | bool | None
Tipado más allá de PEP 695
Hay otras mejoras discretas que alivian código típico. El decorador @override de PEP 698 permite marcar explícitamente métodos que sobreescriben a una clase base, y los type checkers avisan si rompes el contrato por un cambio de nombre en la superclase. PEP 692 añade Unpack y TypedDict para tipar **kwargs, lo que elimina uno de los agujeros crónicos del tipado estructural en Python. PEP 669 expone una API de monitorización de bajo coste que herramientas como debuggers y profilers pueden aprovechar sin pagar el precio del antiguo sys.settrace.
Son mejoras de gente que vive dentro del código a diario: ninguna es espectacular, todas ahorran fricción.
Migración desde 3.10 u 3.11
El camino es aburrido en el buen sentido. La compatibilidad de librerías principales (Django, FastAPI, SQLAlchemy, pandas, numpy) se alcanzó en cuestión de semanas. Las Docker images oficiales python:3.12-slim son drop-in para casi cualquier Dockerfile. El mayor dolor suele venir de extensiones C que dependen de API privadas de CPython: distutils desapareció, funciones en imp fueron eliminadas y algunos paquetes nicho siguen tarde. Si tu proyecto depende de algo obscuro, conviene revisar la matriz de wheels antes de planificar el upgrade.
Mi receta: actualizar pyproject.toml para exigir >=3.12, añadir 3.12 a la matriz de CI en paralelo con la versión actual, dejar que pasen los tests durante una semana y sólo entonces retirar la versión antigua. Para proyectos todavía en 3.9 el aviso es diferente: esa rama llega a end of life en octubre de 2025 y acumular dos saltos mayores en poco tiempo multiplica riesgos. Mejor saltar ya a 3.12 y dejar 3.13 para cuando free-threading se estabilice.
El panorama en diciembre de 2024
Con 3.13 ya publicado en octubre, la pregunta honesta es si saltarse 3.12 tiene sentido. La respuesta corta es no: 3.13 aporta el intérprete experimental sin GIL, un JIT igualmente experimental y un REPL reescrito, pero sus mejoras de rendimiento en modo estándar son discretas y la build free-threaded todavía tiene overhead medible en código single-thread. 3.12 es el último peldaño sin sobresaltos antes de que el GIL deje de ser una asunción universal, y es donde deben estar los proyectos que priorizan estabilidad hoy. La transición posterior será más interesante —y más política, porque recompone contratos de concurrencia que llevan décadas en pie—, pero 3.12 te deja en buena forma para afrontarla sin deuda pendiente.